// OpenNetworks_v01.js 
// Javascript source file for OpenNetworks for Google Maps (v .01) API
// for displaying networks with custom Google Maps interface
//
//  Author:  James M. Hughes
//  Date  :  10/15/2010
//
//  N.B. Conforms to Google Maps API v3 (http://code.google.com/apis/maps/documentation/javascript/)
/*
  This software is distributed under the MIT License:

  The MIT License

    Copyright (c) 2010  James M. Hughes 

    Permission is hereby granted, free of charge, to any person obtaining a copy
    of this software and associated documentation files (the "Software"), to deal
    in the Software without restriction, including without limitation the rights
    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    copies of the Software, and to permit persons to whom the Software is
    furnished to do so, subject to the following conditions:

    The above copyright notice and this permission notice shall be included in
    all copies or substantial portions of the Software.

    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    THE SOFTWARE.
*/

/* GLOBAL VARS */

// DEF'ed constants for map types (access via mapTypes.MAPTYPENAME)
// All defined custom map types should be "registered" in this object
var customMapTypes = {BLANK: 'blank'};

// from and to nodes for shortest path calculation
var path_nodes = new Array();
var from_node = -1;
var to_node = -1;

/* CUSTOM CLASSES */

/**
*    Network constructor - 
*    Builds a network object that contains the specified nodes and edges, placing
*    the network in the HTML div div_id in a Google Maps viewer with
*    options as provided.
*    @constructor
*
*    @param div_id  Id of HTML element (div) where map will reside
*    @param nodes  Array of Node objects
*    @param edges  Array of Edge objects
*    @param map_type, if not null, must be implementation of CustomMapType interface
*    @param map_opts JSON object of map options (see Google Maps API ref.)
*    @param latlng_mapper Mapper from coordinate space to LatLngBounds (type CoordToLatLngBounds)
*
*    @author James M. Hughes
**/
function Network(div_id, nodes, edges, map_type, map_opts, latlng_mapper) {
  // Class members

  // Member functions
  this.initNodes = initNodes;
  this.initEdges = initEdges;
  this.initialize = initialize;
  this.showNodes = showNodes;
  this.hideNodes = hideNodes;

  // More member functions
  this.bindEventToNode = bindEventToNode;
  this.bindEventToNodes = bindEventToNodes;
  this.addControlElement = addControlElement;

  // updater functions
  this.updateEdges = updateEdges;

  // Member data
  // Div id where map is stored
  this.div_id = div_id;

  // Actual google map
  this.map = null;

  // Map options
  if (map_opts != null) {
    this.map_opts = map_opts; 
  }
  else { // set some default options
    this.map_opts = {
      backgroundColor: "#dddddd",
      zoom: 4,
      disableDoubleClickZoom: 0,
      scrollwheel: 0,
      center: new google.maps.LatLng(50,-115),
      mapTypeControlOptions: {
        mapTypeIds: [google.maps.MapTypeId.ROADMAP]
        },
      mapTypeControl: true,
      };
  }

  this.map_type = map_type;

  if (this.map_type != null ) {
    this.map_opts.customMapType = this.map_type;
    this.map_opts.mapTypeControlOptions.mapTypeIds.push(this.map_type.getID());
  }

  // Initialize map
  this.initialize(this.map_opts);

  // nodes
  this.nodes = nodes;
  // Actual node objects
  this.Nodes = [];
  // edges
  this.edges = edges;
  // Actual edge objects
  this.Edges = [];

  // Properties
  this.nodesVisible = true;
  this.edgesVisible = false;

  // set mapper
  this.latlng_mapper = latlng_mapper;

  // set nodes
  if (this.nodes != null) {
    // initialize
    this.initNodes();
  }

  // set edges
  if (this.edges != null) {
    // initialize
    this.initEdges();
  }
}

/** Binds an event to a single node (Node object), specified by the given index (looks in 
*   the object's node list to find the node; returns false if node does not exist
*
*   @param idx  Index of node
*   @param eventType  (string) indicates what event should be listened for (i.e., 'click', 'drag')
*   @param func  function reference; to be executed on event (may be empty function, but not null)
*   @returns true, on success; false otherwise
*   @methodOf Network
**/
function bindEventToNode(idx, eventType, func) {
  if (this.nodes == null || this.nodes.length < idx+1) {
    return false; 
  }

  // at this point, node must exist, so bind event
  // first, ensure we have an actual function
  if (func == null) {
    return false;
  }

  // now bind event to object
  try {
    google.maps.event.addListener(this.Nodes[idx], eventType, func);
  }
  catch (er) {
    return false;
  }

  return true;
}

/** Binds an event to all nodes (Node objects) in the network
*
*   @param eventType  (string) indicates what event should be listened for (i.e., 'click', 'drag')
*   @param func  function reference; to be executed on event (may be empty function, but not null)
*   @returns true, on success; false otherwise
*   @methodOf Network
**/
function bindEventToNodes(eventType, func) {
  if (this.nodes == null || func == null) {
    return false;
  }

  for (i=0; i < this.Nodes.length; i++) {
    success = this.bindEventToNode(i, eventType, func);

    // if ever we fail, we fail altogether
    if (!success) {
      return false;
    }
  }

  // got this far, ok
  return true;
}

/** Adds a control element to the map, specifying position and element
*   @memberOf Network
*
*   @param position  One of static values google.maps.ControlPosition.* (see Google Maps API v3)
*   @param elt       HTML element to be placed in map controls
*
**/
// TODO:  error handling here
function addControlElement(position, elt) {
  this.map.controls[position].insertAt(0, elt); 
}

/**
* initializes a list of nodes using the array input
* nodes should have three fields:  name, xcoord, ycoord
*   @param nodeArray  Array of Node objects
*   @methodOf Network
**/
function initNodes() {
  // initialize the array to zeros to get correct-length Array
  for (var i = 0; i < this.nodes.length; i++) {
    this.Nodes[i] = 0;
  }

  // go through each node
  for (var i = 0; i < this.nodes.length; i++) {
    // check to see whether this node has an id
    if (this.nodes[i].id == null) {
      // if no id present, set equal to position in node array
      this.nodes[i].id = i;
    }

    // find the lat/lng bounds for this object
    var bounds = this.latlng_mapper.map(this.nodes[i].xcoord, this.nodes[i].ycoord);

    // possible node icons:
    // http://maps.google.com/images/srpr/nav_logo13.png
    // http://maps.gstatic.com/mapfiles/red_stars.png (or /stars.png)
    //var image = new google.maps.MarkerImage("http://www.google.com/mapfiles/circle.png", new google.maps.Size(8,8), new google.maps.Point(0,0));

    var curNode = new google.maps.Marker({position: bounds.getSouthWest(), map: this.map, title: this.nodes[i].name});

    // add node to array at correct index
    this.Nodes[this.nodes[i].id] = curNode;
  }
}

/** 
* Initializes this Network's edges using edge array input
* Each input edge should have values 'from' and 'to' as indices indicating which
* nodes are connected by this edge (using the ordering of the input nodes)
* and may include an optional 'weight'
* @param edgeArray Array of Edge objects
* @methodOf Network
**/
function initEdges() {
  // options for how to draw edges
  var edgeOpts = {
    //strokeColor: "#FF0000",
    strokeOpacity: 0.5,
    //strokeWeight: 2,
    clickable: false,
    zIndex: 0 
  };

  // go through each edge, find corresponding nodes, get their LatLng coords and
  // use these to set the path of the edge
  for (i=0; i < this.edges.length; i++) {
    from_idx = this.edges[i].from;
    to_idx = this.edges[i].to;

    var weight = 1;

    if (this.edges[i].weight != undefined) {
      weight = this.edges[i].weight;
    }

    // map this weight to a color between blue (low) and red (high)
    /*
    red = Math.pow(weight,0.5);
    if (weight < 0.5) {
      red = 0;
    }
    //green = -2*Math.pow(weight-0.5,2)+0.5;
    green = Math.pow(weight,0.25);
    if (weight < 0.35 || weight > 0.65) {
      green = 0;
    }
    blue = Math.pow(weight-1,2);
    if (weight > 0.5) {
      blue = 0;
    }
    */
    red = weight;
    green = 0;
    blue = 1-weight;
    //color = rgb2hex8(red,green,blue);
    color = "0000ff";

    edgeOpts['strokeColor'] = '#' + color;
    var line_weight = 10 * weight;
    if (line_weight < 1) {
      line_weight = 1;
    }

    edgeOpts['strokeWeight'] = line_weight;

    edge = new google.maps.Polyline(edgeOpts);

    // set path and map
    from_bound = this.Nodes[from_idx].getPosition();
    to_bound = this.Nodes[to_idx].getPosition();

    edge.setPath(Array(from_bound, to_bound));
    edge.setMap(this.map);

    // add edge to array
    this.Edges.push(edge);
  }
}

/**
* Updates the current edge array with the edges in the input array
* Assumes numerical edge indices correspond to the order of nodes in the node array
* @param edgeArray Array of Edge objects
* @methodOf Network
**/
function updateEdges(edgeArray) {
  // clear old edges
  for (i = 0; i < this.Edges.length; i++) {
    this.Edges[i].setMap(null);
  }

  this.Edges = new Array();

  // add new edges
  this.initEdges(edgeArray);
}

/**
* Initialize a new Network
* @param map_opts JSON object with map options
* @memberOf Network
**/
function initialize(map_opts) {

  this.map = new google.maps.Map(document.getElementById(this.div_id), map_opts);

  if (map_opts.customMapType != null) {
    this.map.mapTypes.set(map_opts.customMapType.getID(), map_opts.customMapType.getMapType());
    this.map.setMapTypeId(map_opts.customMapType.getID());
  }
  else { // default to roadmap
    this.map.setMapTypeId(google.maps.MapTypeId.ROADMAP);
  }

  //var test = new BlankCustomMapType('custom');
  //this.map.mapTypes.set(test.getID(), test.getMapType());

  // set the map type to use as first entry in map_opts, else default
  /*
  if (map_opts.mapTypeControlOptions != null && map_opts.mapTypeControlOptions.mapTypeIds != null) {
    this.map.setMapTypeId(map_opts.mapTypeControlOptions.mapTypeIds[0]);
  }
  */
 
  // insert any additional control elements
  /* TODO:  commented out 10/18/10 to move this functionality somewhere else, i.e., as additional fn or param 
  this.map.controls[google.maps.ControlPosition.TOP_RIGHT].insertAt(0, createRadioButtonControlDiv('Sparse', 'sparse', true, 'update_network_edges(airline_nodes)'));
  this.map.controls[google.maps.ControlPosition.TOP_RIGHT].insertAt(0, createRadioButtonControlDiv('Mid', 'sparse', false, 'update_network_edges(airline_nodes_mid)'));
  this.map.controls[google.maps.ControlPosition.TOP_RIGHT].insertAt(0, createRadioButtonControlDiv('Dense', 'sparse', false, 'update_network_edges(airline_nodes_dense)'));
  this.map.controls[google.maps.ControlPosition.TOP_RIGHT].insertAt(0, createRadioButtonControlDiv('Global', 'sparse', false, 'update_network_edges(airline_nodes_global)'));
  this.map.controls[google.maps.ControlPosition.TOP_RIGHT].insertAt(0, createCheckboxControlDiv("Show nodes", false, "showNetworkNodes(this.checked);"));
  this.map.controls[google.maps.ControlPosition.RIGHT].insertAt(0, createTextDiv("", "num_edges"));
  */
} 

/**
*  Show the nodes in this network.
*  Provides interactivity function that can be utilized from within the map itself or externally via page controls.
*  @memberOf Network
**/
function showNodes() {
  for (i=0; i < this.Nodes.length; i++) {
    this.Nodes[i].setVisible(true);
  }

  this.nodesVisible = true;
}

/**
*  Hide the nodes in this network.
*  Provides interactivity function that can be utilized from within the map itself or externally via page controls.
*  @memberOf Network
**/
function hideNodes() {
  for (i=0; i < this.Nodes.length; i++) {
    this.Nodes[i].setVisible(false);
  }

  this.nodesVisible = false;
}

//TODO: implement this "superclass" and add the necessary methods to register map types w/in Network
/** Custom MapType interface for displaying maps of any kind underneath Network.
*   The subclass itself should describe how to build a MapType of the correct type (see Google Maps API v3)
*   when calling the getMapType method of the object, using the options specified, if necessary.
*
*   Any subclass should have implemented the getID() and getMapType() methods.
*
*   @constructor
*   @param opts (optional / JSON object) Options specified for this map type
*
**/
function CustomMapType(ID, opts) {
  this.ID = ID;
  this.options = opts;

  this.getID = function() { return this.ID; };
  this.getOptions = function() { return this.options };
  this.getMapType = function() { return null; };
}

/** Custom MapType that displays a blank (grey) background underneath a Network object.
*
*   @constructor
*   @param ID  (String) ID -- the map will register this map type according to 
**/
function BlankCustomMapType(ID) {
  this.ID = ID;

  this.getID = function() { return this.ID; };

  // Create a custom ImageMapType that returns a blank tile
  this.getMapType = function() {
    var copts = {
      getTileUrl: function(coord, zoom) {
        url = "http://maps.gstatic.com/intl/en_us/mapfiles/transparent.png";
        
        return url;
      },
      tileSize: new google.maps.Size(256,256),
      isPng: true,
      minZoom: 2,
      maxZoom: 16,
      name: 'blank',
      alt: 'blank map type'
    };

    return new google.maps.ImageMapType(copts);
  };
}

/**
* Custom ImageOverlay type (see Google Maps API v3 ref.)
* 
*
*
**/
function c_ImageOverlay(bounds, image, map, idx, text, select_image, source_image, dest_image) {
  // init
  this.bounds_ = bounds;
  this.image_ = image;
  this.map_ = map;
  this.idx_ = idx;
  this.select_image_ = select_image;
  this.source_image_ = source_image;
  this.dest_image_ = dest_image;

  this.div_ = null;
  this.text_ = text;
  this.text_div_ = null;
  this.info_box_div_ = null;
  this.info_box_open_ = false;

  this.setMap(map);
}

// sub-class
c_ImageOverlay.prototype = new google.maps.OverlayView();

/**
*  Event handler when c_ImageOverlay object added to HTML
**/
c_ImageOverlay.prototype.onAdd = function() {
  // create the div elt
  var div = document.createElement("DIV");
  // border stuff here really just for testing
  div.style.border = "none";
  div.style.position = "absolute";

  // anchor for linking to events
  var a = document.createElement("A");
  a.href = "#";
  a.onclick = new Function("", "return registerNodeClick(" + this.idx_ + ");");

  var img = document.createElement("img");
  img.id = "img" + this.idx_;
  img.src = this.image_;
  img.style.width="100%";
  img.style.height="100%";
  img.style.zIndex = "1";
  img.style.marginLeft = "0px";
  img.style.marginTop = "0px";
  img.style.position = "absolute";

  a.appendChild(img);
  div.appendChild(a);

  var text_div = document.createElement("DIV");
  text_div.style.position = "relative";
  text_div.style.marginLeft = "97%";
  text_div.style.marginTop = "25%";
  text_div.style.width = "100px";
  text_div.style.zIndex = "-1";
  text_div.style.display = "";
  //text_div.onclick = new Function("", "return registerNodeClick(" + this.idx_ + ");");
  text_div.innerHTML = this.text_; 

  this.text_div_ = text_div;

  div.appendChild(this.text_div_);
  this.info_box_div_ = createInfoBox();
  div.appendChild(this.info_box_div_);

  //div.innerHTML = "<a href='#' onclick='alert(" + this.idx_ + "); return false'><img src='" + this.image_ + "' border='0'></a>";

  // set the div
  this.div_ = div;

  // place this in one of the map's panes
  var panes = this.getPanes();
  panes.overlayImage.appendChild(div);
}

// hides text
c_ImageOverlay.prototype.hideText = function() {
  this.text_div_.style.display = "none";
}
// shows text
c_ImageOverlay.prototype.showText = function() {
  this.text_div_.style.display = ""; 
}

// image reset function
c_ImageOverlay.prototype.resetImage = function() {
  this.div_.getElementsByTagName("img")[0].src = this.image_;
}

// select image set function
c_ImageOverlay.prototype.setSelectImage = function() {
  this.div_.getElementsByTagName("img")[0].src = this.select_image_;
}

// set source image function
c_ImageOverlay.prototype.setSourceImage = function() {
  this.div_.getElementsByTagName("img")[0].src = this.source_image_;
}

// set destination image function
c_ImageOverlay.prototype.setDestImage = function() {
  this.div_.getElementsByTagName("img")[0].src = this.dest_image_;
}

// Shows a node-based info box
c_ImageOverlay.prototype.toggleInfoBox = function() {
  if (!this.info_box_open_) {
    this.openInfoBox();
    this.setSelectImage();
    this.info_box_open_ = true;
  }
  else {
    //this.info_box_div_.display = "none";
    this.closeInfoBox();
    this.resetImage();
    this.info_box_open_ = false;
  }
}

/**
* Handler for drawing a c_ImageOverlay object
**/
c_ImageOverlay.prototype.draw = function() {
  var overlayProjection = this.getProjection();

  var sw = overlayProjection.fromLatLngToDivPixel(this.bounds_.getSouthWest());
  var ne = overlayProjection.fromLatLngToDivPixel(this.bounds_.getNorthEast());

  var div = this.div_;
  div.style.left = sw.x + 'px';
  div.style.top = ne.y + 'px';
  
  // calculate width/height
  var width = (ne.x - sw.x);
  var height = (ne.y - sw.y);

  // correct if minimum width/height not reached
  
  // equality check to make div square; set to max
  if (width != height) {
    width = Math.max(width, height);
    height = width;
  }

  // correct if minimum width not reached
  if (width < 16) {
    width = 16;
    height = 16;
  }

  // correct if maximum width/height reached
  if (width > 128) {
    width = 128;
    height = 128;
  }

  div.style.width = width + 'px';
  div.style.height = height + 'px';

  // TESTESTEST added 7/2/10 -- see if we can place the div right on top of the
  // upper-right-hand corner in order to place it over the point of any edges connecting
  // this node to others
  div.style.top = ne.y - (height/2);
  div.style.left = ne.x - (width/2);
}

// onRemove function
c_ImageOverlay.prototype.onRemove = function() {
  this.div_.parentNode.removeChild(this.div_);
  this.div_ = null;
}

// HIDING AND SHOWING this overlay type
c_ImageOverlay.prototype.hide = genericHide;
c_ImageOverlay.prototype.show = genericShow;
c_ImageOverlay.prototype.getCenter = genericGetCenter;

/**
* Custom text overlay type (see Google Maps API v3 ref.)
**/
function c_TextOverlay(bounds, text, map) {
  // Initialize properties
  this.bounds_ = bounds;
  this.text_ = text;
  //this.text_ = text;
  this.map_ = map;

  // set this onAdd
  this.div_ = null;

  this.setMap(map);
}

// sub-class this!
c_TextOverlay.prototype = new google.maps.OverlayView();

// onAdd function
c_TextOverlay.prototype.onAdd = function() {
  // create the div elt
  var div = document.createElement("DIV");
  // border stuff here really just for testing
  div.style.border = "none";
  //div.style.border = "black solid";
  //div.style.borderWidth = "2px";
  div.style.position = "absolute";

  // just use the text we stored
  div.innerHTML = this.text_;

  // set the div
  this.div_ = div;

  // place this in one of the map's panes
  var panes = this.getPanes();
  panes.overlayShadow.appendChild(div);
}

// draw function
c_TextOverlay.prototype.draw = function() {
  var overlayProjection = this.getProjection();

  var sw = overlayProjection.fromLatLngToDivPixel(this.bounds_.getSouthWest());
  var ne = overlayProjection.fromLatLngToDivPixel(this.bounds_.getNorthEast());

  var div = this.div_;
  div.style.left = sw.x + 'px';
  div.style.top = ne.y + 'px';

  // calculate width and height
  var width = (ne.x - sw.x);
  var height = (ne.y - sw.y);

  // equality check to make div square; set to max
  if (width != height) {
    width = Math.max(width, height);
    height = width;
  }

  div.style.width = width + 'px';
  div.style.height = height + 'px';
  
  // some debug output
  //document.getElementById("test_div").innerHTML = div.style.left + " " + div.style.top + " " + div.style.width + " " + div.style.height + " " + this.bounds_ + " " + ne.x + " " + sw.y;
}

// onRemove function
c_TextOverlay.prototype.onRemove = function() {
  this.div_.parentNode.removeChild(this.div_);
  this.div_ = null;
}

// HIDING AND SHOWING this overlay type
c_TextOverlay.prototype.hide = genericHide;
c_TextOverlay.prototype.show = genericShow;

function genericHide() {
  // if this evaluates to false, then the div doesn't even exist (i.e., is null)
  if (this.div_) {
    this.div_.style.visibility = "hidden";
  }
}

function genericShow() {
  if (this.div_) {
    this.div_.style.visibility = "visible";
  }
}

// generic getter for bounds center
function genericGetCenter() {
  if (this.bounds_) {
    //return this.bounds_.getCenter();
    return this.bounds_.getNorthEast();
  }
}

/**** CUSTOM OVERLAY MAP TYPE (c_OverlayMapType) + OPTIONS ****/
// Displays author names in correct positions as overlays on top of map
// Created with assistance from Google Maps API V3 Tutorials
function c_OverlayMapType(tileSize) {
  this.tileSize = tileSize;
}

c_OverlayMapType.prototype.getTile = function(coord, zoom, ownerDocument) {
  var div = ownerDocument.createElement("DIV");
  div.innerHTML = coord;
  div.style.width = this.tileSize.width + 'px';
  div.style.height = this.tileSize.height + 'px';
  div.style.fontSize = '10';
  div.style.borderStyle = 'solid';
  div.style.borderWidth = '1px';
  div.style.borderColor = '#AAAAAA';
  return div;
}

c_OverlayMapType.prototype.name = "Name overlay";
c_OverlayMapType.prototype.alt = "Display author names";

/**** IMAGE MAP TYPE (c_ImageMapType) + OPTIONS ****/
// c_ImageMapTypeOptions 
var c_ImageMapTypeOptions = {
  getTileUrl: function(coord, zoom) {
    url = "http://maps.gstatic.com/intl/en_us/mapfiles/transparent.png";
    
    return url;
  },
  tileSize: new google.maps.Size(512,512),
  isPng: true,
  minZoom: 2,
  maxZoom: 16,
  name: 'Blank',
  alt: 'Blank map type'
};

var c_ImageMapType = new google.maps.ImageMapType(c_ImageMapTypeOptions);

// Shows/hides custom overlay based on value of boolean show
function showNetworkNodes(show) {
  if (show) {
    net.showNodes();
  }
  else {
    net.hideNodes();
  }
}

/** Static method
    Creates a generic checkbox element in a div w/ the specified text and event function
    
    @param outer_text  (String) text to appear in div next to checkbox
    @param checked     (boolean) specify if the box should be checked by default (true) or not (false)
    @param onclick     (String) function performed on firing of click event

    @returns div HTML element
**/
function createCheckboxControlDiv(outer_text, checked, onclick) {
  var div = document.createElement("DIV");
  if (checked) {
    div.innerHTML = "<input type='checkbox' checked onclick='" + onclick + "'>" + outer_text;
  }
  else {
    div.innerHTML = "<input type='checkbox' onclick='" + onclick + "'>" + outer_text;
  }

  // style properties
  div.style.backgroundColor = "#ffffff";
  div.style.fontWeight = "bold";
  div.style.marginRight = "5px";
  div.style.marginTop = "5px";
  div.style.padding = "3px";
  div.style.border = "1px solid #000000";
  
  return div;
}

/** Static method
*  Creates a generic div w/ ID as specified for writing text
*
*   @param inner_text Text inside div
*   @param id  DOM id for this element
* 
*   @returns div HTML element
**/
function createTextDiv(inner_text, id) {
  var div = document.createElement("DIV");

  div.setAttribute("id", id);

  div.innerHTML = inner_text;

  div.style.height = "100px";
  div.style.width = "250px";
  div.style.color = "#000000";
  div.style.font = "12pt Arial";
  div.style.fontWeight = "bold";
  div.style.textAlign = "right";
  div.style.marginRight = "5px";
  div.style.marginTop = "5px";

  return div;
}

/** Static method
*   Creates a generic radio button element in a div w/ the text specified + options
*
*   @param outer_text Text next to radio button
*   @param grp_name  Group name (for tying together with other radio buttons)
*   @param checked  Selected (true) or not (false)
*   @param onclick  (String) javascript function to be executed on firing of click event
*
*   @returns div HTML element
**/
function createRadioButtonControlDiv(outer_text, grp_name, checked, onclick) {
  var div = document.createElement("DIV");
  if (checked) {
    div.innerHTML = "<input type='radio' name='" + grp_name + "' checked onclick='" + onclick + "'>" + outer_text;
  }
  else {
    div.innerHTML = "<input type='radio' name='" + grp_name + "' onclick='" + onclick + "'>" + outer_text;
  }
  // style properties
  div.style.marginTop = "5px";
  div.style.marginRight = "5px";
  div.style.padding = "3px";
  div.style.height = "19px";
  div.style.backgroundColor = "#ffffff";
  div.style.border = "1px solid black";
  div.style.fontWeight = "bold";
  return div;
}

/** Static method
*   Creates a generic button element in a div w/ the text specified + options
*
*   @param button_text Text within button
*   @param outer_text Text next to button (may be empty string)
*   @param onclick  (String) javascript function to be executed on firing of click event
*
*   @returns div HTML element
**/
function createButtonControlDiv(button_text, outer_text, onclick) {
  var div = document.createElement("DIV");
  div.innerHTML += "<input type='button' value='" + button_text + "' onclick='" + onclick + "'>";

  // style properties
  div.style.marginTop = "5px";
  div.style.marginRight = "5px";
  div.style.padding = "3px";
  div.style.height = "19px";
  //div.style.backgroundColor = "#ffffff";
  //div.style.border = "1px solid black";
  div.style.fontWeight = "bold";
  return div;
}

// Creates a generic anchor element in a div with specified text, link + options
function createAnchorControlDiv(outer_text, link_text, link, onclick) {
  var div = document.createElement("DIV");
  div.innerHTML = outer_text + "<a href='" + link + "' onclick='" + onclick + " return false;'>" + link_text + "</a>";
  // style properties
  //div.style.backgroundColor = "#ffffff";
  //div.style.border = "1px";
  //div.style.borderColor = "#000000";
  return div;
}

// Creates a generic anchor element in a div with specified text, link + options
function createAnchorControlDivStyled(outer_text, link_text, link, onclick, idx) {
  var div = document.createElement("DIV");
  div.innerHTML = outer_text + "<p><a style='color: #f3f2e8' onclick='" + onclick + " return false;'>" + link_text + "</a></p>";
  // get this div's paragraph
  var p = div.children[0];
  // style the anchor
  p.style.color = "#f3f2e8";
  p.style.font = "13pt Arial";
  p.style.fontWeight = "bold";
  p.style.width = "160px";

  // figure out how to vertically center this
  p.style.marginLeft = "7px";
  p.style.cssFloat = "left";

  // get this div's anchor
  //var a = div.children[0].children[0];

  //a.color = "#f3f2e8";

  // add control buttons
  ndiv = document.createElement("DIV");
  ndiv.style.width = "65px";
  ndiv.style.height = "50px";
  ndiv.style.cssFloat = "left";
  ndiv.style.marginRight = "0px";
  ndiv.style.marginTop = "10px";

  var onclick_fn = "registerInfoClick(" + idx + "); return false;";
  var center_fn = "centerMap(imgmap, authorNodes[" + idx + "].getCenter()); return false;";
  ndiv.innerHTML += "<a href='#' onclick='" + center_fn + "'><img src='images/center_button_info.png'></a>";
  ndiv.innerHTML += "<a href='#' onclick='" + onclick_fn + "'><img src='images/more_info_button_info.png'></a>";

  // set anchor spacing
  ndiv.children[0].style.marginBottom = "10px";

  div.appendChild(ndiv);

  return div;
}

/** Centers a map on the google.maps.LatLng coordinates provided as input
*  This is a utility method that isn't bound to a particular object, but can be used in 
*  a variety of instances.
*  @param map  google.maps.Map object
*  @param LatLng  Latitude/Longitude coordinates
* 
**/
function centerMap(map, LatLng) {
  map.setCenter(LatLng);
}

// Gets the (indices of the) neighbors of a particular node by idx
function getNodeNeighbors(idx) {
  // Go through the edges, find the ones involving this node, and save the names
  var idxArray = new Array();

  // walk through edge array
  for (i=0; i < origEdges.length; i++) {
    if (origEdges[i].from == idx) {
      idxArray.push(origEdges[i].to);
    }
    else if (origEdges[i].to == idx) {
      idxArray.push(origEdges[i].from);
    } 
  }

  return idxArray;
}

// Global click handler
function registerClick(idx, is_map_click) {
  // first, handle the case that the click was from the info div
  // SIDEBAR CLICK
  if (!is_map_click) {
    // if we are in the correct mode
    if (node_view_mode) {
      // if there was a click on the map *and* there is an open info box
      if (was_map_click && open_info_box != -1) {
        // toggle the open info box
        authorNodes[open_info_box].toggleInfoBox();
        open_info_box = -1;

        // open this info box
        toggleInfoBox(idx, authorIdxToRank[idx]);
        open_info_box = idx;
      }
      // if we didn't see a map click and there is an info box open other than this one
      else if (open_info_box != -1 && open_info_box != idx) {
        toggleInfoBox(open_info_box, authorIdxToRank[idx]);
        open_info_box = -1;

        // open this info box
        toggleInfoBox(idx, authorIdxToRank[idx]);
        open_info_box = idx;
      }
      // if this info box is open, close it
      else if (open_info_box == idx) {
        toggleInfoBox(idx, authorIdxToRank[idx]);
        open_info_box = -1;
      }
      else {
        // open this info box
        toggleInfoBox(idx, authorIdxToRank[idx]);
        open_info_box = idx;
      }
    }

    // reset click state
    was_map_click = false;
  }
  // second, handle the case that the click was from the map div
  // MAP CLICK
  else {
    if (node_view_mode) {
      // if the last click was not on the map *and* there is an open info box
      if (!was_map_click && open_info_box != -1) {
        // toggle the open info box
        toggleInfoBox(open_info_box, authorIdxToRank[open_info_box]);

        // open this info box
        authorNodes[idx].toggleInfoBox();
        open_info_box = idx;
      }
      // otherwise, we might need to close a node info box not equal to the current one
      else if (open_info_box != -1 && open_info_box != idx) {
        authorNodes[open_info_box].toggleInfoBox();
        open_info_box = -1;

        // open this info box
        authorNodes[idx].toggleInfoBox();
        open_info_box = idx;
      }
      // or we might need to close THIS info box
      else if (open_info_box == idx) {
        authorNodes[open_info_box].toggleInfoBox();
        open_info_box = -1;
      }
      else {
        // open this info box
        authorNodes[idx].toggleInfoBox();
        open_info_box = idx;
      }
    }
    else if (shortest_path_mode) {
      setShortestPathNode(idx); 
    }
  
    // reset click state
    was_map_click = true;
  }
}

/** Constructs a mapper between coordinate space and latitude-longitude space,
*   which allows for pretty much any transformation of the coordinates to be
*   taken into account.
*   By default, linear scaling and offsets are supported; custom mappings can
*   be constructed by subclassing this class (incl. high-dim. to low-dim. mappings).
*
*   The expectation is that a call to CoordToLatLngMapper.call() returns the correct mapping.
*
*   Note that this superclass alone will not do the trick -- it must be implemented.
*
*   @constructor
*   @author James M. Hughes
**/
function CoordToLatLngMapper() {
  // function that should be the mapper
  this.map = null;
}

/** Linearly maps a point in 2-space to Latitude-Longitude space
*   using the formula lng = xcoord*xscale_factor, lat = ycoord*yscale_factor.
*   @constructor
*   @param xscale_factor Amount of scaling applied to x-coordinate
*   @param yscale_factor Amount of scaling applied to y-coordinate
*
*   @author James M. Hughes
**/
function LinearCoordToLatLngMapper(xscale_factor, yscale_factor) {
  this.xscale_factor = xscale_factor;
  this.yscale_factor = yscale_factor;

  /** Maps a points to google.maps.LatLng, given a point 
  *   @param xcoord x-coordinate of point
  *   @param ycoord y-coordinate of point
  *   @returns google.maps.LatLng
  **/
  this.map = function(xcoord, ycoord) {
    var bound = new google.maps.LatLng(xcoord*this.xscale_factor, ycoord*this.yscale_factor);
    return bound;
  };
}

/** Maps a point in 2-space to a point on a circle in Latitude-Longitude space with radius 1, scaled
*   by specified value.  Uses the formula lng = (xcoord / sqrt(xcoord^2 + ycoord^2))*scale_factor, 
*   lat = (ycoord / sqrt(xcoord^2 + ycoord^2))*scale_factor.
*   @constructor
*   @param scale_factor Amount of scaling applied to coordinates (i.e., radius of circle in lat/lng space)
*
*   @author James M. Hughes
**/
function CircularCoordToLatLngMapper(scale_factor) {
  this.scale_factor = scale_factor;

  /** Maps a points to google.maps.LatLng, given a point 
  *   @param xcoord x-coordinate of point
  *   @param ycoord y-coordinate of point
  *   @returns google.maps.LatLng
  **/
  this.map = function(xcoord, ycoord) {
    // first, find the 2-norm of the vector
    var norm = Math.pow(xcoord,2) + Math.pow(ycoord,2);
    norm = Math.sqrt(norm);
    var bound = new google.maps.LatLng((xcoord/norm)*this.scale_factor, (ycoord/norm)*this.scale_factor);
    return bound;
  };
}

/** Maps a point to google.maps.LatLngBounds (i.e., square that represents the
*   given point as an area on the map), given the point, a mapper,
*   and an appropriate offset
*   @constructor
*   @param mapper Object subclassed from CoordToLatLngMapper
*   @param pad_factor padding (in Latitude/Longitude space) between corners of the square area
*
*   @author James M. Hughes
**/
function CoordToLatLngBoundsMapper(mapper, pad_factor) {
  this.mapper = mapper;
  this.pad_factor = pad_factor;

  /** Maps the point to bounds, given the mapper specified in the Class constructor
  *   @param point (possibly variable length) coordinates of point, passed as individual coordinates, not as array
  *          (e.g., map(xcoord, ycoord, zcoord))
  *   @returns google.maps.LatLngBounds
  **/
  // TODO:  this function needs some serious error handling in case of bad inputs
  this.map = function() {
    // variable-length arguments mean we need to handle this carefully
    // get the southwest bound
    swBound = this.mapper.map.apply(this.mapper, arguments);

    // add the pad_factor to each point
    for (var i=0; i < arguments.length; i++) {
      arguments[i] += pad_factor;
    }

    // get the northeast bound
    neBound = this.mapper.map.apply(this.mapper, arguments);

    return new google.maps.LatLngBounds(swBound, neBound);
  };
}

/** The Node class **/
/** Creates a Node object, given the name of the node, an x-coordinate,
*   and a y-coordinate.  The coordinates are not presumed to be in any
*   space in particular, and the way in which nodes are laid out in the
*   network is determined by the mapper used to convert points to latitude/longitude
*   @constructor
*
*   @param name  display name of node (string)
*   @param xcoord  x-coordinate of node
*   @param ycoord  y-coordinate of node
*   @param id  (optional) integer id number of node (used for identifying this node
*              when creating edges, etc.  If left blank, will be determined by order of Nodes
*              in input array to Network, once Node is added to Network.
**/
// TODO:  maybe use getter/setter methods here?
function Node(name, xcoord, ycoord, id) {
  this.name = name;
  this.xcoord = xcoord;
  this.ycoord = ycoord;
  this.id = id;
}

/** The Edge class **/
/** Creates an Edge object, specifying an edge from node "from" to node "to,"
*   with weight specified by (optional) weight.  If no weight is specified,
*   the weight defaults to 1.
*   @constructor
*   @param from Integer id of origin node
*   @param to   Integer id of destination node
*   @param weight Scalar node weight (can be real-valued, must be positive (i.e., 0 weight means no edge)
**/
function Edge(from, to, weight) {
  this.from = from;
  this.to = to;

  if (weight == null) {
    this.weight = 1;
  }
  else if (weight > 0) {
    this.weight = weight;
  }
  else { // might eventually want to change this behavior
    this.weight = 1;
  }
}
